package common.db.dao.hibernate;

import java.io.Serializable;
import java.lang.reflect.Method;
import java.sql.Connection;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.util.ArrayList;
import java.util.Collection;
import java.util.List;
import java.util.Map;
import java.util.Objects;

import javax.persistence.Entity;

import org.apache.logging.log4j.LogManager;
import org.hibernate.HibernateException;
import org.hibernate.JDBCException;
import org.hibernate.LockMode;
import org.hibernate.LockOptions;
import org.hibernate.ObjectNotFoundException;
import org.hibernate.PropertyValueException;
import org.hibernate.SQLQuery;
import org.hibernate.Session;
import org.hibernate.SessionFactory;
import org.hibernate.Transaction;
import org.hibernate.exception.ConstraintViolationException;
import org.hibernate.exception.DataException;
import org.hibernate.exception.LockAcquisitionException;
import org.hibernate.jdbc.Work;
import org.hibernate.transform.Transformers;

import common.db.dao.Dao;
import common.db.dao.DaoConstraintException;
import common.db.dao.DaoLockException;
import common.db.dao.DaoPropertyException;
import common.db.dao.DaoUtil;
import common.db.jdbc.Jdbc;

import core.config.Factory;
import core.exception.PhysicalException;
import core.exception.ThrowableUtil;

/**
 * DAOベースクラス
 *
 * @author Tadashi Nakayama
 * @version 1.0.0
 */
public abstract class BaseDao implements Dao {

	/** NO WAIT排他 */
	private static final LockOptions NO_WAIT_UPGRADE;

	static {
		NO_WAIT_UPGRADE = new LockOptions(LockMode.UPGRADE_NOWAIT);
		NO_WAIT_UPGRADE.setTimeOut(LockOptions.NO_WAIT);
	}

	/** コンフィグ */
	private final Config config;

	/** 排他フラグ */
	private boolean noWait = false;

	/**
	 * コンストラクタ
	 * @param cfg コンフィグ
	 */
	protected BaseDao(final Config cfg) {
		this.config = cfg;
	}

	/**
	 * セションファクトリ取得
	 * @return セションファクトリ
	 */
	protected final SessionFactory getSessionFactory() {
		return this.config.getSessionFactory();
	}

	/**
	 * 排他待ち設定
	 *
	 * @param val 排他待ちフラグ
	 */
	@Override
	public final void setNoWait(final boolean val) {
		this.noWait = val;
	}

	/**
	 * 排他待ち取得
	 *
	 * @return 排他待ちフラグ
	 */
	public final boolean isNoWait() {
		return this.noWait;
	}

	/**
	 * プライマリキーによる検索
	 * @param <T> ジェネリックス
	 *
	 * @param cls 読込モデルのクラス
	 * @param id プライマリキーオブジェクト
	 * @return 検索結果オブジェクト
	 */
	@Override
	public final <T> T findByIdWithLock(final Class<T> cls, final Serializable id) {
		LockOptions lo = NO_WAIT_UPGRADE;
		if (!this.noWait) { lo = LockOptions.UPGRADE; }

		if (!isEntity(cls)) {
			List<Object> param = new ArrayList<>();
			return selectWithLock(cls, toSelectQuery(cls, id, param, lo),
					param.toArray(new Object[param.size()]));
		}
		return find(cls, id, lo);
	}

	/**
	 * プライマリキーによる検索
	 * @param <T> ジェネリックス
	 *
	 * @param cls 読込モデルのクラス
	 * @param id プライマリキーオブジェクト
	 * @return 検索結果オブジェクト
	 */
	@Override
	public final <T> T findById(final Class<T> cls, final Serializable id) {
		if (!isEntity(cls)) {
			List<Object> param = new ArrayList<>();
			return select(cls, toSelectQuery(cls, id, param, LockOptions.READ),
					param.toArray(new Object[param.size()]));
		}
		return find(cls, id, LockOptions.READ);
	}

	/**
	 * 選択クエリ取得
	 * @param <T> ジェネリックス
	 * @param cls 読込モデルのクラス
	 * @param id プライマリキーオブジェクト
	 * @param param パラメタ設定リスト
	 * @param lo ロックオブジェクト
	 * @return 選択クエリ
	 */
	private <T> String toSelectQuery(final Class<T> cls, final Serializable id,
			final List<Object> param, final LockOptions lo) {
		String ret = "SELECT * FROM " + DaoUtil.getTableName(cls)
			+ EntityUtil.toWhereString(id, param, DaoUtil.getIdMethod(cls));

		if (NO_WAIT_UPGRADE == lo) {
			ret = ret + " " + this.config.getDialect().getForUpdateNowaitString();
		} else if (LockOptions.UPGRADE == lo) {
			ret = ret + " " + this.config.getDialect().getForUpdateString();
		}
		return ret;
	}

	/**
	 * プライマリキーによる検索
	 * @param <T> ジェネリックス
	 *
	 * @param cls 読込モデルのクラス
	 * @param id プライマリキーオブジェクト
	 * @param lo ロックオプション
	 * @return 検索結果オブジェクト
	 */
	private <T> T find(final Class<T> cls, final Serializable id, final LockOptions lo) {
		try {
			// セションオブジェクト取得
			Session session = getSession();
			// トランザクション開始
			beginTransaction();
			// 読込
			Object obj = session.load(cls, id, lo);
			session.evict(obj);
			return cls.cast(obj);
		} catch (ObjectNotFoundException ex) {
			return null;
		} catch (HibernateException ex) {
			ThrowableUtil.error(ex);
			throw new PhysicalException(ex);
		}
	}

	/**
	 * レコード挿入
	 *
	 * @param item 挿入対象モデル
	 */
	@Override
	public final void insert(final Serializable item) {
		// NULL可項目NULL化
		toNull(item);
		try {
			if (isEntity(item.getClass())) {
				// セションオブジェクト取得
				Session session = getSession();
				if (session.contains(item) && session.isReadOnly(item)) {
					session.evict(item);
				}
				// トランザクション開始
				beginTransaction();
				// 保存
				session.save(item);
				flushSession(session, item);
			} else {
				setId(item);
				List<Object> param = new ArrayList<>();
				execute(EntityUtil.toInsertSql(item, param),
						param.toArray(new Object[param.size()]));
			}
		} catch (LockAcquisitionException ex) {
			LogManager.getLogger().info(ex.getMessage());
			throw new DaoLockException(ex, this.noWait);
		} catch (ConstraintViolationException ex) {
			LogManager.getLogger().warn("Item={}, Message={}", item, ex.getMessage());
			throw new DaoConstraintException(ex, this.noWait);
		} catch (PropertyValueException ex) {
			// not null例外
			LogManager.getLogger().warn("Item={}, Message={}", item, ex.getMessage());
			throw new DaoPropertyException(ex);
		} catch (HibernateException ex) {
			ThrowableUtil.error(ex);
			throw new PhysicalException(ex);
		}
	}

	/**
	 * Id設定
	 * @param item 挿入対象モデル
	 */
	private void setId(final Serializable item) {
		if (!DaoUtil.isEmbeddedId(item.getClass())) {
			String name = DaoUtil.getSequenceName(item.getClass());
			if (!Objects.toString(name, "").isEmpty()) {
				Method mt = Factory.getMethod(item.getClass(), "setId", Long.class);
				Factory.invoke(item, mt, Long.valueOf(sequence(name)));
			}
		}
	}

	/**
	 * レコード追加または更新
	 *
	 * @param item 更新対象モデル
	 */
	@Override
	public final void merge(final Serializable item) {
		if (!isEntity(item.getClass())) {
			throw new IllegalArgumentException(item.getClass().getName());
		}

		// NULL可項目NULL化
		toNull(item);
		try {
			// セションオブジェクト取得
			Session session = getSession();
			if (session.contains(item) && session.isReadOnly(item)) {
				session.evict(item);
			}
			// トランザクション開始
			beginTransaction();
			// 更新
			session.merge(item);
			flushSession(session, item);
		} catch (LockAcquisitionException ex) {
			LogManager.getLogger().info(ex.getMessage());
			throw new DaoLockException(ex, this.noWait);
		} catch (ConstraintViolationException ex) {
			LogManager.getLogger().warn("Item={}, Message={}", item, ex.getMessage());
			throw new DaoConstraintException(ex, this.noWait);
		} catch (PropertyValueException ex) {
			// not null例外
			LogManager.getLogger().warn("Item={}, Message={}", item, ex.getMessage());
			throw new DaoPropertyException(ex);
		} catch (HibernateException ex) {
			ThrowableUtil.error(ex);
			throw new PhysicalException(ex);
		}
	}

	/**
	 * レコード更新
	 *
	 * @param item 更新対象モデル
	 * @return 更新された場合 true を返す。対象なしの場合 false を返す。
	 */
	@Override
	public final boolean update(final Serializable item) {
		// NULL可項目NULL化
		toNull(item);
		try {
			if (isEntity(item.getClass())) {
				// セションオブジェクト取得
				Session session = getSession();
				if (session.contains(item) && session.isReadOnly(item)) {
					session.evict(item);
				}
				// トランザクション開始
				beginTransaction();
				// 更新
				session.update(item);
				flushSession(session, item);
			} else {
				List<Object> param = new ArrayList<>();
				execute(EntityUtil.toUpdateSql(item, param),
						param.toArray(new Object[param.size()]));
			}
			return true;
		} catch (LockAcquisitionException ex) {
			LogManager.getLogger().info(ex.getMessage());
			throw new DaoLockException(ex, this.noWait);
		} catch (ConstraintViolationException ex) {
			LogManager.getLogger().warn("Item={}, Message={}", item, ex.getMessage());
			throw new DaoConstraintException(ex, this.noWait);
		} catch (PropertyValueException ex) {
			// not null例外
			LogManager.getLogger().warn("Item={}, Message={}", item, ex.getMessage());
			throw new DaoPropertyException(ex);
		} catch (JDBCException ex) {
			ThrowableUtil.error(ex);
			throw new PhysicalException(ex);
		} catch (HibernateException ex) {
			if (isUpdateFailed(ex)) {
				LogManager.getLogger().info(ex.getMessage());
				return false;
			}
			ThrowableUtil.error(ex);
			throw new PhysicalException(ex);
		}
	}

	/**
	 * レコード削除
	 *
	 * @param item 削除対象モデル
	 * @return 削除された場合 true を返す。
	 */
	@Override
	public final boolean delete(final Serializable item) {
		try {
			if (isEntity(item.getClass())) {
				// セションオブジェクト取得
				Session session = getSession();
				if (session.contains(item) && session.isReadOnly(item)) {
					session.evict(item);
				}
				// トランザクション開始
				beginTransaction();
				// 削除処理
				session.delete(item);
				flushSession(session, item);
			} else {
				List<Object> param = new ArrayList<>();
				execute(EntityUtil.toDeleteSql(item, param),
						param.toArray(new Object[param.size()]));
			}
			return true;
		} catch (LockAcquisitionException ex) {
			LogManager.getLogger().info(ex.getMessage());
			throw new DaoLockException(ex, this.noWait);
		} catch (JDBCException ex) {
			ThrowableUtil.error(ex);
			throw new PhysicalException(ex);
		} catch (HibernateException ex) {
			if (isUpdateFailed(ex)) {
				LogManager.getLogger().info(ex.getMessage());
				return false;
			}
			ThrowableUtil.error(ex);
			throw new PhysicalException(ex);
		}
	}

	/**
	 * ネイティブSQLクエリ実行
	 * @param <T> ジェネリックス
	 *
	 * @param cls 返却クラスタイプ
	 * @param query SQL
	 * @param vals パラメタ値
	 * @return モデルオブジェクト
	 */
	@Override
	public final <T> T select(final Class<T> cls, final String query, final Object... vals) {
		try {
			// セションオブジェクト取得
			Session session = getSession();
			// トランザクション開始
			beginTransaction();

			List<?> list = createQuery(session, getRecordClass(cls),
					query, vals).setReadOnly(true).setMaxResults(1).list();
			if (list != null && !list.isEmpty()) {
				if (isEntity(cls)) {
					session.evict(list.get(0));
				} else if (cls != null && !isJavaClass(cls)) {
					return EntityUtil.toEntity(Factory.<Map<String, Object>>cast(list.get(0)), cls);
				}
				return Factory.cast(list.get(0));
			}
			return null;
		} catch (ObjectNotFoundException | DataException ex) {
			return null;
		} catch (HibernateException ex) {
			ThrowableUtil.error(ex);
			throw new PhysicalException(ex);
		}
	}

	/**
	 * ネイティブSQLクエリ実行（排他）
	 * @param <T> ジェネリックス
	 * @param cls 返却クラスタイプ
	 * @param query SQL
	 * @param vals パラメタ値
	 * @return モデルオブジェクト
	 */
	@Override
	public final <T> T selectWithLock(final Class<T> cls,
			final String query, final Object... vals) {
		if (isJavaClass(cls)) {
			throw new IllegalArgumentException(cls.getName());
		}

		try {
			// セションオブジェクト取得
			Session session = getSession();
			// トランザクション開始
			beginTransaction();

			List<?> list = createQuery(session, getRecordClass(cls), addFor(query), vals).list();
			if (list == null || list.isEmpty()) {
				return null;
			} else if (list.size() == 1) {
				if (isEntity(cls)) {
					session.evict(list.get(0));
				} else if (cls != null) {
					return EntityUtil.toEntity(Factory.<Map<String, Object>>cast(list.get(0)), cls);
				}
				return Factory.cast(list.get(0));
			}

			throw new IllegalStateException("More than one record is selected.");
		} catch (ObjectNotFoundException | DataException ex) {
			return null;
		} catch (LockAcquisitionException ex) {
			LogManager.getLogger().info(ex.getMessage());
			throw new DaoLockException(ex, this.noWait);
		} catch (HibernateException ex) {
			ThrowableUtil.error(ex);
			throw new PhysicalException(ex);
		}
	}

	/**
	 * 更新クエリ追加
	 * @param query クエリ
	 * @return SQL
	 */
	private String addFor(final String query) {
		if (this.noWait) {
			return query + " " + this.config.getDialect().getForUpdateNowaitString();
		}
		return query + " " + this.config.getDialect().getForUpdateString();
	}

	/**
	 * ネイティブSQLクエリ実行
	 * @param <T> ジェネリックス
	 *
	 * @param cls 返却クラスタイプ
	 * @param query SQL
	 * @param vals パラメタ値
	 * @return モデルリスト
	 */
	@Override
	public final <T> List<T> selectAll(final Class<T> cls,
			final String query, final Object... vals) {
		try {
			// セションオブジェクト取得
			Session session = getSession();
			// トランザクション開始
			beginTransaction();

//			String sql = query.replaceAll(" [Ff][Oo][Rr] .*", "");
			List<?> list = createQuery(
					session, getRecordClass(cls), query, vals).setReadOnly(true).list();
			if (cls != null && !isEntity(cls) && !isJavaClass(cls)) {
				return EntityUtil.toEntityList(Factory.<List<Map<String, Object>>>cast(list), cls);
			}
			return Factory.cast(list);
		} catch (ObjectNotFoundException | DataException ex) {
			return null;
		} catch (HibernateException ex) {
			ThrowableUtil.error(ex);
			throw new PhysicalException(ex);
		}
	}

	/**
	 * 取得クラス取得
	 * @param cls クラス
	 * @return 取得クラス
	 */
	private Class<?> getRecordClass(final Class<?> cls) {
		return (cls != null && !isEntity(cls) && !isJavaClass(cls)) ? Map.class : cls;
	}

	/**
	 * SQL更新実行
	 * @param query クエリ
	 * @param vals パラメタ値
	 * @return 処理件数
	 */
	@Override
	public final int execute(final String query, final Object... vals) {
		try {
			// セションオブジェクト取得
			Session session = getSession();
			// トランザクション開始
			beginTransaction();

			session.flush();
			SQLQuery sql = createQuery(session, null, query, vals);
			return sql.executeUpdate();
		} catch (LockAcquisitionException ex) {
			LogManager.getLogger().info(ex.getMessage());
			throw new DaoLockException(ex, this.noWait);
		} catch (ConstraintViolationException ex) {
			LogManager.getLogger().info(ex.getMessage());
			throw new DaoConstraintException(ex, this.noWait);
		} catch (HibernateException ex) {
			ThrowableUtil.error(ex);
			throw new PhysicalException(ex);
		}
	}

	/**
	 * JDBC実行処理
	 *
	 * @param work 実行クラス
	 */
	@Override
	public final void doWork(final JdbcWork work) {
		if (HibernateJdbcWork.class.isInstance(work)) {
			HibernateJdbcWork.class.cast(work).setDialect(this.config.getDialect());
		}
		try {
			Session session = getSession();
			beginTransaction();
			session.doWork(new WorkWrap(work));
		} catch (HibernateException ex) {
			ThrowableUtil.error(ex);
			throw new PhysicalException(ex);
		}
	}

	/**
	 * シーケンス取得クエリ取得
	 * @param name シーケンス名
	 * @return シーケンス取得クエリ
	 */
	protected final String getSequenceNextValString(final String name) {
		return this.config.getDialect().getSequenceNextValString(name);
	}

	/**
	 * 文字列項目が空文字の場合nullにする。
	 * @param obj エンティティBean
	 */
	private void toNull(final Serializable obj) {
		for (final Method m : obj.getClass().getMethods()) {
			if (Factory.isGetter(m) && String.class.equals(m.getReturnType())
					&& DaoUtil.isNullable(m)) {
				String val = String.class.cast(Factory.invoke(obj, m));
				if (val != null && val.trim().isEmpty()) {
					Method mt = Factory.getMethod(obj.getClass(),
							"set" + Factory.toItemName(m), String.class);
					Factory.invoke(obj, mt, (String)null);
				}
			}
		}
	}

	/**
	 * Queryオブジェクト作成
	 *
	 * @param <T> ジェネリクス
	 * @param session セション
	 * @param cls 返却クラスタイプ
	 * @param query SQL条件文
	 * @param vals パラメタ値
	 * @return Queryオブジェクト
	 */
	private <T> SQLQuery createQuery(final Session session, final Class<T> cls,
			final String query, final Object... vals) {
		SQLQuery qry = session.createSQLQuery(query);
		qry.setCacheable(false);

		if (Map.class.equals(cls)) {
			qry.setResultTransformer(Transformers.ALIAS_TO_ENTITY_MAP);
		} else if (List.class.equals(cls)) {
			qry.setResultTransformer(Transformers.TO_LIST);
		} else if (isEntity(cls)) {
			qry.addEntity(cls);
		}

		String[] name = qry.getNamedParameters();
		if (name.length == 1 && 1 < vals.length) {
			qry.setParameterList(name[0], vals);
		} else {
			for (int i = 0, s = 1; vals != null && i < vals.length; i++) {
				if (Collection.class.isInstance(vals[i])) {
					qry.setParameterList(String.valueOf(s++), Collection.class.cast(vals[i]));
				} else if (vals[i] != null && vals[i].getClass().isArray()) {
					qry.setParameterList(String.valueOf(s++), Object[].class.cast(vals[i]));
				} else {
					qry.setParameter(String.valueOf(s++), vals[i]);
				}
			}
		}
		return qry;
	}

	/**
	 * エンティティ確認
	 * @param cls クラス
	 * @return エンティティの場合 true を返す。
	 */
	private boolean isEntity(final Class<?> cls) {
		return cls != null && cls.getAnnotation(Entity.class) != null;
	}

	/**
	 * javaクラス確認
	 * @param cls クラス
	 * @return javaクラスの場合 true を返す。
	 */
	private boolean isJavaClass(final Class<?> cls) {
		return cls != null && cls.getName().startsWith("java");
	}

	/**
	 * セション取得
	 *
	 * @return セション
	 */
	protected abstract Session getSession();

	/**
	 * セション取得
	 *
	 * @return トランザクション
	 */
	protected abstract Transaction beginTransaction();

	/**
	 * 同期化
	 *
	 * @param session セション
	 * @param item 挿入対象モデル
	 */
	protected abstract void flushSession(final Session session, final Serializable item);

	/**
	 * 更新用処理確認
	 *
	 * @param ex 例外
	 * @return 未更新時 true
	 */
	protected final boolean isUpdateFailed(final HibernateException ex) {
		return ex != null && ex.getMessage() != null
				&& ex.getMessage().startsWith("Batch update row count wrong: 0");
	}

	/**
	 * 実行クラスラップ
	 *
	 * @author Tadashi Nakayama
	 * @version 1.0.0
	 */
	private static final class WorkWrap implements Work {
		/** 実行クラス */
		private final JdbcWork work;

		/**
		 * コンストラクタ
		 *
		 * @param val 実行クラス
		 */
		public WorkWrap(final JdbcWork val) {
			this.work = val;
		}

		/**
		 * @see org.hibernate.jdbc.Work#execute(java.sql.Connection)
		 */
		@Override
		public void execute(final Connection conn) throws SQLException {
			this.work.execute(conn);
		}
	}

	/**
	 * シーケンス取得クラス
	 * @author Tadashi Nakayama
	 * @version 1.0.0
	 */
	protected static class SequenceWork implements Work {

		/** クエリ */
		private final String query;
		/** ステートメント */
		private volatile PreparedStatement psmt;
		/** シーケンス */
		private volatile long sequence;

		/**
		 * コンストラクタ
		 * @param sql クエリ
		 */
		public SequenceWork(final String sql) {
			this.query = sql;
		}

		/**
		 * シーケンス取得
		 * @return シーケンス
		 */
		public final long getSequence() {
			return this.sequence;
		}

		/**
		 * @see org.hibernate.jdbc.Work#execute(java.sql.Connection)
		 */
		@Override
		public final void execute(final Connection conn) throws SQLException {
			this.sequence = 0;

			if (this.psmt == null || this.psmt.isClosed()) {
				this.psmt = Jdbc.wrap(conn).readonlyStatement(this.query);
			}

			try (ResultSet rs = this.psmt.executeQuery()) {
				if (!rs.next()) {
					throw new IllegalStateException();
				}
				this.sequence = rs.getLong(1);
			}
		}

		/**
		 * クローズ処理
		 */
		public final void close() {
			if (this.psmt != null) {
				try {
					this.psmt.close();
				} catch (SQLException ex) {
					ThrowableUtil.warn(ex);
				} finally {
					this.psmt = null;
				}
			}
		}
	}
}
